热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

尾部|柜台_Java并发线程池篇附场景分析

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。作者:汤圆个人博客

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。


作者:汤圆

个人博客:javalover.cc


前言

前面我们在创建线程时,都是直接new Thread();

这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,CPU爆满等问题

幸运的是,Java里面有线程池的概念,而线程池的核心框架,就是我们今天的主题,Executor

接下来,就让我们一起畅游在Java线程池的海洋中吧



本节会用银行办业务的场景来对比介绍线程池的核心概念,这样理解起来会很轻松



简介

Executor是线程池的核心框架;

和它相对应的有一个辅助工厂类Executors,这个类提供了许多工厂方法,用来创建各种各样的线程池,下面我们先看下几种常见的线程池

// 容量固定的线程池
Executor fixedThreadPool = Executors.newFixedThreadPool(5);
// 容量动态增减的线程池
Executor cachedThreadPool = Executors.newCachedThreadPool();
// 单个线程的线程池
Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
// 基于调度机制的线程池(不同于上面的线程池,这个池创建的任务不会立马执行,而是定期或者延时执行)
Executor scheduledThreadPool = Executors.newScheduledThreadPool(5);

上面这些线程池的区别主要就是线程数量的不同以及任务执行的时机

下面让我们开始吧



文章如果有问题,欢迎大家批评指正,在此谢过啦



目录


  1. 线程池的底层类ThreadPoolExecutor
  2. 为啥阿里不建议使用 Executors来创建线程池?
  3. 线程池的生命周期 ExecutorService

正文


1. 线程池的底层类 ThreadPoolExecutor

在文章开头创建的几个线程池,内部都是有调用ThreadPoolExecutor这个类的,如下所示

public static ExecutorService newFixedThreadPool(int nThreads)
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());

这个类是Exexutor的一个实现类&#xff0c;关系图如下所示&#xff1a;


  • 其中Executors就是上面介绍的辅助工厂类&#xff0c;用来创建各种线程池

  • 接口ExecutorService是Executor的一个子接口&#xff0c;它对Executor进行了扩展&#xff0c;原有的Executor只能执行任务&#xff0c;而ExecutorService还可以管理线程池的生命周期&#xff08;下面会介绍&#xff09;

所以我们先来介绍下这个底层类&#xff0c;它的完整构造参数如下所示&#xff1a;

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

在介绍这些参数之前&#xff0c;我们可以先举个生活中的例子-去银行办业务&#xff1b;然后对比着来理解&#xff0c;会比较清晰

&#xff08;图中绿色的窗口表示一直开着&#xff09;


  • corePoolSize&#xff1a; 核心线程数&#xff0c;就是一直存在的线程&#xff08;不管用不用&#xff09;&#xff1b;&#61;》窗口的1号窗和2号窗
  • maximumPoolSize&#xff1a;最大线程数&#xff0c;就是最多可以创建多少个线程&#xff1b;&#61;》窗口的1&#xff0c;2&#xff0c;3&#xff0c;4号窗
  • keepAliveTime&#xff1a;多余的线程&#xff08;最大线程数 减去 核心线程数&#xff09;空闲时存活的时间&#xff1b;&#61;》窗口的3号窗和4号窗空闲的时间,如果超过keepAliveTime&#xff0c;还没有人来办业务&#xff0c;那么就会暂时关闭3号窗和4号窗
  • workQueue: 工作队列&#xff0c;当核心线程数都在执行任务时&#xff0c;再进来的任务就会添加到工作队列中&#xff1b;&#61;》椅子&#xff0c;客户等待区
  • threadFactory&#xff1a;线程工厂&#xff0c;用来创建初始的核心线程&#xff0c;下面会有介绍&#xff1b;
  • handler&#xff1a;拒绝策略&#xff0c;当所有线程都在执行任务&#xff0c;且工作队列也满时&#xff0c;再进来的任务就会被执行拒绝策略&#xff08;比如丢弃&#xff09;&#xff1b;&#61;》左下角的那个小人

基本的工作流程如下所示&#xff1a;

上面的参数我们着重介绍下工作队列和拒绝策略&#xff0c;线程工厂下面再介绍

工作队列&#xff1a;


  • ArrayBlockingQueue&#xff1a;
    • 数组阻塞队列&#xff0c;这个队列是一个有界队列&#xff0c;遵循FIFO&#xff0c;尾部插入&#xff0c;头部获取
    • 初始化时需指定队列的容量 capacity
    • 类比到上面的场景&#xff0c;就是椅子的数量为初始容量capacity
  • LinkedBlockingQueue&#xff1a;
    • 链表阻塞队列&#xff0c;这是一个无界队列&#xff0c;遵循FIFO&#xff0c;尾部插入&#xff0c;头部获取
    • 初始化时可不指定容量&#xff0c;此时默认的容量为Integer.MAX_VALUE&#xff0c;基本上相当于无界了&#xff0c;此时队列可一直插入&#xff08;如果处理任务的速度小于插入的速度&#xff0c;时间长了就有可能导致OOM)
    • 类比到上面的场景&#xff0c;就是椅子的数量为Integer.MAX_VALUE
  • SynchronousQueue&#xff1a;
    • 同步队列&#xff0c;阻塞队列的特殊版&#xff0c;即没有容量的阻塞队列&#xff0c;随进随出&#xff0c;不做停留
    • 类比到上面的场景&#xff0c;就是椅子的数量为0&#xff0c;来一个人就去柜台办理&#xff0c;如果柜台满了&#xff0c;就拒绝
  • PriorityBlockingQueue
    • 优先级阻塞队列&#xff0c;这是一个无界队列&#xff0c;不遵循FIFO&#xff0c;而是根据任务自身的优先级顺序来执行
    • 初始化可不指定容量&#xff0c;默认11&#xff08;既然有容量&#xff0c;怎么还是无界的呢&#xff1f;因为它添加元素时会进行扩容&#xff09;
    • 类比到上面的场景&#xff0c;就是新来的可以插队办理业务&#xff0c;好比各种会员

拒绝策略&#xff1a;


  • AbortPolicy&#xff08;默认&#xff09;&#xff1a;
    • 中断策略&#xff0c;抛出异常 RejectedExecutionException&#xff1b;
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则抛出 RejectedExecutionException&#xff08;系统会停止运行&#xff0c;但是不会退出&#xff09;
  • DiscardPolicy&#xff1a;
    • 丢弃策略&#xff0c;丢掉新来的任务
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接丢掉&#xff08;看任务的重要程度&#xff0c;不重要的任务可以用这个策略&#xff09;
  • DiscardOldestPolicy&#xff1a;
    • 丢弃最旧策略&#xff0c;丢掉最先进入队列的任务&#xff08;有点残忍了&#xff09;&#xff0c;然后再次执行插入操作
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接丢掉队列头部的任务&#xff0c;并再次插入任务
  • CallerRunsPolicy&#xff1a;
    • 回去执行策略&#xff0c;让新来的任务返回到调用它的线程中去执行&#xff08;比如main线程调用了executors.execute(task)&#xff0c;那么就会将task返回到main线程中去执行&#xff09;
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接返回该任务&#xff0c;到调用它的线程中去执行

2. 为啥阿里不建议使用 Executors来创建线程池&#xff1f;

原话如下&#xff1a;

我们可以写几个代码来测试一下

先测试FixedThreadPool&#xff0c;代码如下&#xff1a;

public class FixedThreadPoolDemo
public static void main(String[] args)
// 创建一个固定容量为10的线程池&#xff0c;核心线程数和最大线程数都为10
ExecutorService executorService &#61; Executors.newFixedThreadPool(10);
for (int i &#61; 0; i < 1_000_000; i&#43;&#43;)
try
executorService.execute(()->
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();

);
catch (Exception e)
e.printStackTrace();




这里我们需对VM参数做一点修改&#xff0c;让问题比较容易复现

如下所示&#xff0c;我们添加-Xmx8m -Xms8m到VM option中(-Xmx8m&#xff1a;JVM堆的最大内存为8M&#xff0c; -Xms8m&#xff0c;JVM堆的初始化内存为8M)&#xff1a;

此时点击运行&#xff0c;就会发现报错如下&#xff1a;

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.jalon.concurrent.chapter6.FixedThreadPoolDemo.main(FixedThreadPoolDemo.java:21)

我们来分析下原因


  • 首先&#xff0c;newFixedThreadPool内部用的工作队列为LinkedBlockingQueue&#xff0c;这是一个无界队列&#xff08;容量最大为Integer.MAX_VALUE&#xff0c;基本上可一直添加任务&#xff09;
  • 如果任务插入的速度&#xff0c;超过了任务执行的速度&#xff0c;那么队列肯定会越来越长&#xff0c;最终导致OOM


CachedThreadPool也是类似的原因&#xff0c;只不过它是因为最大线程数为Integer.MAX_VALUE&#xff1b;


所以当任务插入的速度&#xff0c;超过了任务执行的速度&#xff0c;那么线程的数量会越来越多&#xff0c;最终导致OOM


那我们要怎么创建线程池呢&#xff1f;

可以用ThreadPoolExecutor来自定义创建&#xff0c;通过为最大线程数和工作队列都设置一个边界&#xff0c;来限制相关的数量&#xff0c;如下所示&#xff1a;

public class ThreadPoolExecutorDemo
public static void main(String[] args)
ExecutorService service &#61; new ThreadPoolExecutor(
1, // 核心线程数
1, // 最大线程数
60L, // 空闲时间
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), // 数组工作队列&#xff0c;长度1
new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略&#xff1a;丢弃
for (int i &#61; 0; i < 1_000_000; i&#43;&#43;)
// 通过这里的打印信息&#xff0c;我们可以知道循环了3次
// 原因就是第一次的任务在核心线程中执行&#xff0c;第二次的任务放到了工作队列&#xff0c;第三次的任务被拒绝执行
System.out.println(i);
service.execute(()->
// 这里会报异常&#xff0c;是因为执行了拒绝策略&#xff08;达到了最大线程数&#xff0c;队列也满了&#xff0c;此时新进来的任务就会执行拒绝策略&#xff09;
// 这里需要注意的是&#xff0c;抛出异常后&#xff0c;代码并不会退出&#xff0c;而是卡在异常这里&#xff0c;包括主线程也会被卡住(这个是默认的拒绝策略&#xff09;
// 我们可以用其他的拒绝策略&#xff0c;比如DiscardPolicy,此时代码就会继续往下执行
System.out.println(Thread.currentThread().getName());
);

try
Thread.sleep(1000);
System.out.println("主线程 sleep ");
catch (InterruptedException e)
e.printStackTrace();




3. 线程池的生命周期 ExecutorService

Executor接口默认只有一个方法void execute(Runnable command);&#xff0c;用来执行任务

任务一旦开启&#xff0c;我们就无法再去插手了&#xff0c;比如停止、监控等

此时就需要ExecutorService登场了&#xff0c;它是Executor的一个子接口&#xff0c;对其进行了扩展&#xff0c;方法如下&#xff1a;

public interface ExecutorService extends Executor
void shutdown(); // 优雅地关闭&#xff0c;这个关闭会持续一段时间&#xff0c;以等待已经提交的任务去执行完成&#xff08;但是在shutdown之后提交的任务会被拒绝&#xff09;
List<Runnable> shutdownNow(); // 粗暴地关闭&#xff0c;这个关闭会立即关闭所有正在执行的任务&#xff0c;并返回工作队列中等待的任务
boolean isShutdown();
boolean isTerminated();
// 用来等待线程的执行
// 如果在timeout之内&#xff0c;线程都执行完了&#xff0c;则返回true&#xff1b;
// 如果等了timeout&#xff0c;还没执行完&#xff0c;则返回false&#xff1b;
// 如果timeout之内&#xff0c;线程被中断&#xff0c;则抛出中断异常
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

从上面可以看到&#xff0c;线程池的生命周期分三步&#xff1a;


  1. 运行&#xff1a;创建后就开始运行
  2. 关闭&#xff1a;调用shutdown进入关闭状态
  3. 已终止&#xff1a;所有线程执行完毕

总结


  1. 线程池的底层类 ThreadPoolExecutor&#xff1a;核心概念就是核心线程数、最大线程数、工作队列、拒绝策略
  2. 为啥阿里不建议使用 Executors来创建线程池&#xff1f;&#xff1a;因为会导致OOM&#xff0c;解决办法就是自定义ThreadPoolExecutor&#xff0c;为最大线程数和工作队列设置边界
  3. 线程池的生命周期ExecutorService&#xff1a;运行状态&#xff08;创建后进入&#xff09;、关闭状态&#xff08;shutdown后进入&#xff09;、已终止状态&#xff08;所有线程都执行完成后进入&#xff09;

参考内容&#xff1a;


  • 《Java并发编程实战》
  • 《实战Java高并发》
  • newFixedThreadPool的弊端&#xff1a;https://my.oschina.net/langwanghuangshifu/blog/3208320
  • 银行办业务的场景参考&#xff1a;https://b23.tv/ygGjTH

后记

愿你的意中人亦是中意你之人


推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文介绍了在Java中gt、gtgt、gtgtgt和lt之间的区别。通过解释符号的含义和使用例子,帮助读者理解这些符号在二进制表示和移位操作中的作用。同时,文章还提到了负数的补码表示和移位操作的限制。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
author-avatar
子新宥梅93
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有